/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.websocket;

import org.apache.coyote.http11.upgrade.UpgradeOutbound;
import org.apache.tomcat.util.buf.B2CConverter;
import org.apache.tomcat.util.res.StringManager;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;

/**
 * Provides the means to write WebSocket messages to the client. All methods
 * that write to the client (or update a buffer that is later written to the
 * client) are synchronized to prevent multiple threads trying to write to the
 * client at the same time.
 *
 * @deprecated Replaced by the JSR356 WebSocket 1.1 implementation and will be
 * removed in Tomcat 8.0.x.
 */
@Deprecated
public class WsOutbound {

	public static final int DEFAULT_BUFFER_SIZE = 8192;
	private static final StringManager sm =
			StringManager.getManager(Constants.Package);
	/**
	 * This state lock is used rather than synchronized methods to allow error
	 * handling to be managed outside of the synchronization else deadlocks may
	 * occur such as https://bz.apache.org/bugzilla/show_bug.cgi?id=55524
	 */
	private final Object stateLock = new Object();

	private UpgradeOutbound upgradeOutbound;
	private StreamInbound streamInbound;
	private ByteBuffer bb;
	private CharBuffer cb;
	private boolean closed = false;
	private Boolean text = null;
	private boolean firstFrame = true;

	public WsOutbound(UpgradeOutbound upgradeOutbound,
	                  StreamInbound streamInbound) {
		this(upgradeOutbound, streamInbound, DEFAULT_BUFFER_SIZE,
				DEFAULT_BUFFER_SIZE);
	}

	public WsOutbound(UpgradeOutbound upgradeOutbound, StreamInbound streamInbound,
	                  int byteBufferSize, int charBufferSize) {
		this.upgradeOutbound = upgradeOutbound;
		this.streamInbound = streamInbound;
		this.bb = ByteBuffer.allocate(byteBufferSize);
		this.cb = CharBuffer.allocate(charBufferSize);
	}

	/**
	 * Adds the data to the buffer for binary data. If a textual message is
	 * currently in progress that message will be completed and a new binary
	 * message started. If the buffer for binary data is full, the buffer will
	 * be flushed and a new binary continuation fragment started.
	 *
	 * @param b The byte (only the least significant byte is used) of data to
	 *          send to the client.
	 * @throws IOException If a flush is required and an error occurs writing
	 *                     the WebSocket frame to the client
	 */
	public void writeBinaryData(int b) throws IOException {
		try {
			synchronized (stateLock) {
				if (closed) {
					throw new IOException(sm.getString("outbound.closed"));
				}

				if (bb.position() == bb.capacity()) {
					doFlush(false);
				}
				if (text == null) {
					text = Boolean.FALSE;
				} else if (Boolean.TRUE.equals(text)) {
					// Flush the character data
					flush();
					text = Boolean.FALSE;
				}
				bb.put((byte) (b & 0xFF));
			}
		} catch (IOException ioe) {
			// Any IOException is terminal. Make sure the inbound side knows
			// that something went wrong.
			// The exception handling needs to be outside of the sync to avoid
			// possible deadlocks (e.g. BZ55524) when triggering the inbound
			// close as that will execute user code
			streamInbound.doOnClose(Constants.STATUS_CLOSED_UNEXPECTEDLY);
			throw ioe;
		}
	}

	/**
	 * Adds the data to the buffer for textual data. If a binary message is
	 * currently in progress that message will be completed and a new textual
	 * message started. If the buffer for textual data is full, the buffer will
	 * be flushed and a new textual continuation fragment started.
	 *
	 * @param c The character to send to the client.
	 * @throws IOException If a flush is required and an error occurs writing
	 *                     the WebSocket frame to the client
	 */
	public void writeTextData(char c) throws IOException {
		try {
			synchronized (stateLock) {
				if (closed) {
					throw new IOException(sm.getString("outbound.closed"));
				}

				if (cb.position() == cb.capacity()) {
					doFlush(false);
				}

				if (text == null) {
					text = Boolean.TRUE;
				} else if (Boolean.FALSE.equals(text)) {
					// Flush the binary data
					flush();
					text = Boolean.TRUE;
				}
				cb.append(c);
			}
		} catch (IOException ioe) {
			// Any IOException is terminal. Make sure the Inbound side knows
			// that something went wrong.
			// The exception handling needs to be outside of the sync to avoid
			// possible deadlocks (e.g. BZ55524) when triggering the inbound
			// close as that will execute user code
			streamInbound.doOnClose(Constants.STATUS_CLOSED_UNEXPECTEDLY);
			throw ioe;
		}
	}

	/**
	 * Flush any message (binary or textual) that may be buffered and then send
	 * a WebSocket binary message as a single frame with the provided buffer as
	 * the payload of the message.
	 *
	 * @param msgBb The buffer containing the payload
	 * @throws IOException If an error occurs writing to the client
	 */
	public void writeBinaryMessage(ByteBuffer msgBb) throws IOException {

		try {
			synchronized (stateLock) {
				if (closed) {
					throw new IOException(sm.getString("outbound.closed"));
				}

				if (text != null) {
					// Empty the buffer
					flush();
				}
				text = Boolean.FALSE;
				doWriteBytes(msgBb, true);
			}
		} catch (IOException ioe) {
			// Any IOException is terminal. Make sure the Inbound side knows
			// that something went wrong.
			// The exception handling needs to be outside of the sync to avoid
			// possible deadlocks (e.g. BZ55524) when triggering the inbound
			// close as that will execute user code
			streamInbound.doOnClose(Constants.STATUS_CLOSED_UNEXPECTEDLY);
			throw ioe;
		}
	}

	/**
	 * Flush any message (binary or textual) that may be buffered and then send
	 * a WebSocket text message as a single frame with the provided buffer as
	 * the payload of the message.
	 *
	 * @param msgCb The buffer containing the payload
	 * @throws IOException If an error occurs writing to the client
	 */
	public void writeTextMessage(CharBuffer msgCb) throws IOException {

		try {
			synchronized (stateLock) {
				if (closed) {
					throw new IOException(sm.getString("outbound.closed"));
				}

				if (text != null) {
					// Empty the buffer
					flush();
				}
				text = Boolean.TRUE;
				doWriteText(msgCb, true);
			}
		} catch (IOException ioe) {
			// Any IOException is terminal. Make sure the Inbound side knows
			// that something went wrong.
			// The exception handling needs to be outside of the sync to avoid
			// possible deadlocks (e.g. BZ55524) when triggering the inbound
			// close as that will execute user code
			streamInbound.doOnClose(Constants.STATUS_CLOSED_UNEXPECTEDLY);
			throw ioe;
		}
	}

	/**
	 * Flush any message (binary or textual) that may be buffered.
	 *
	 * @throws IOException If an error occurs writing to the client
	 */
	public void flush() throws IOException {
		try {
			synchronized (stateLock) {
				if (closed) {
					throw new IOException(sm.getString("outbound.closed"));
				}
				doFlush(true);
			}
		} catch (IOException ioe) {
			// Any IOException is terminal. Make sure the Inbound side knows
			// that something went wrong.
			// The exception handling needs to be outside of the sync to avoid
			// possible deadlocks (e.g. BZ55524) when triggering the inbound
			// close as that will execute user code
			streamInbound.doOnClose(Constants.STATUS_CLOSED_UNEXPECTEDLY);
			throw ioe;
		}
	}

	private void doFlush(boolean finalFragment) throws IOException {
		if (text == null) {
			// No data
			return;
		}
		if (text.booleanValue()) {
			cb.flip();
			doWriteText(cb, finalFragment);
		} else {
			bb.flip();
			doWriteBytes(bb, finalFragment);
		}
	}

	/**
	 * Respond to a client close by sending a close that echoes the status code
	 * and message.
	 *
	 * @param frame The close frame received from a client
	 * @throws IOException If an error occurs writing to the client
	 */
	protected void close(WsFrame frame) throws IOException {
		if (frame.getPayLoadLength() > 0) {
			// Must be status (2 bytes) plus optional message
			if (frame.getPayLoadLength() == 1) {
				throw new IOException();
			}
			int status = (frame.getPayLoad().get() & 0xFF) << 8;
			status += frame.getPayLoad().get() & 0xFF;

			if (validateCloseStatus(status)) {
				// Echo the status back to the client
				close(status, frame.getPayLoad());
			} else {
				// Invalid close code
				close(Constants.STATUS_PROTOCOL_ERROR, null);
			}
		} else {
			// No status
			close(0, null);
		}
	}

	private boolean validateCloseStatus(int status) {

		if (status == Constants.STATUS_CLOSE_NORMAL ||
				status == Constants.STATUS_SHUTDOWN ||
				status == Constants.STATUS_PROTOCOL_ERROR ||
				status == Constants.STATUS_UNEXPECTED_DATA_TYPE ||
				status == Constants.STATUS_BAD_DATA ||
				status == Constants.STATUS_POLICY_VIOLATION ||
				status == Constants.STATUS_MESSAGE_TOO_LARGE ||
				status == Constants.STATUS_REQUIRED_EXTENSION ||
				status == Constants.STATUS_UNEXPECTED_CONDITION ||
				(status > 2999 && status < 5000)) {
			// Other 1xxx reserved / not permitted
			// 2xxx reserved
			// 3xxx framework defined
			// 4xxx application defined
			return true;
		}
		// <1000 unused
		// >4999 undefined
		return false;
	}

	/**
	 * Send a close message to the client
	 *
	 * @param status Must be a valid status code or zero to send no code
	 * @param data   Optional message. If message is defined, a valid status
	 *               code must be provided.
	 * @throws IOException If an error occurs writing to the client
	 */
	public void close(int status, ByteBuffer data) throws IOException {

		try {
			synchronized (stateLock) {
				if (closed) {
					return;
				}

				// Send any partial data we have
				try {
					doFlush(false);
				} finally {
					closed = true;
				}

				upgradeOutbound.write(0x88);
				if (status == 0) {
					upgradeOutbound.write(0);
				} else if (data == null || data.position() == data.limit()) {
					upgradeOutbound.write(2);
					upgradeOutbound.write(status >>> 8);
					upgradeOutbound.write(status);
				} else {
					upgradeOutbound.write(2 + data.limit() - data.position());
					upgradeOutbound.write(status >>> 8);
					upgradeOutbound.write(status);
					upgradeOutbound.write(data.array(), data.position(),
							data.limit() - data.position());
				}
				upgradeOutbound.flush();

				bb = null;
				cb = null;
				upgradeOutbound = null;
			}
		} catch (IOException ioe) {
			// Any IOException is terminal. Make sure the Inbound side knows
			// that something went wrong.
			// The exception handling needs to be outside of the sync to avoid
			// possible deadlocks (e.g. BZ55524) when triggering the inbound
			// close as that will execute user code
			streamInbound.doOnClose(Constants.STATUS_CLOSED_UNEXPECTEDLY);
			throw ioe;
		}
	}

	/**
	 * Send a pong message to the client
	 *
	 * @param data Optional message.
	 * @throws IOException If an error occurs writing to the client
	 */
	public void pong(ByteBuffer data) throws IOException {
		sendControlMessage(data, Constants.OPCODE_PONG);
	}

	/**
	 * Send a ping message to the client
	 *
	 * @param data Optional message.
	 * @throws IOException If an error occurs writing to the client
	 */
	public void ping(ByteBuffer data) throws IOException {
		sendControlMessage(data, Constants.OPCODE_PING);
	}

	/**
	 * Generic function to send either a ping or a pong.
	 *
	 * @param data   Optional message.
	 * @param opcode The byte to include as the opcode.
	 * @throws IOException If an error occurs writing to the client
	 */
	private void sendControlMessage(ByteBuffer data, byte opcode) throws IOException {

		try {
			synchronized (stateLock) {
				if (closed) {
					throw new IOException(sm.getString("outbound.closed"));
				}

				doFlush(false);

				upgradeOutbound.write(0x80 | opcode);
				if (data == null) {
					upgradeOutbound.write(0);
				} else {
					upgradeOutbound.write(data.limit() - data.position());
					upgradeOutbound.write(data.array(), data.position(),
							data.limit() - data.position());
				}

				upgradeOutbound.flush();
			}
		} catch (IOException ioe) {
			// Any IOException is terminal. Make sure the Inbound side knows
			// that something went wrong.
			// The exception handling needs to be outside of the sync to avoid
			// possible deadlocks (e.g. BZ55524) when triggering the inbound
			// close as that will execute user code
			streamInbound.doOnClose(Constants.STATUS_CLOSED_UNEXPECTEDLY);
			throw ioe;
		}
	}

	/**
	 * Writes the provided bytes as the payload in a new WebSocket frame.
	 *
	 * @param buffer        The bytes to include in the payload.
	 * @param finalFragment Do these bytes represent the final fragment of a
	 *                      WebSocket message?
	 * @throws IOException
	 */
	private void doWriteBytes(ByteBuffer buffer, boolean finalFragment)
			throws IOException {

		if (closed) {
			throw new IOException(sm.getString("outbound.closed"));
		}

		// Work out the first byte
		int first = 0x00;
		if (finalFragment) {
			first = first + 0x80;
		}
		if (firstFrame) {
			if (text.booleanValue()) {
				first = first + 0x1;
			} else {
				first = first + 0x2;
			}
		}
		// Continuation frame is OpCode 0
		upgradeOutbound.write(first);

		if (buffer.limit() < 126) {
			upgradeOutbound.write(buffer.limit());
		} else if (buffer.limit() < 65536) {
			upgradeOutbound.write(126);
			upgradeOutbound.write(buffer.limit() >>> 8);
			upgradeOutbound.write(buffer.limit() & 0xFF);
		} else {
			// Will never be more than 2^31-1
			upgradeOutbound.write(127);
			upgradeOutbound.write(0);
			upgradeOutbound.write(0);
			upgradeOutbound.write(0);
			upgradeOutbound.write(0);
			upgradeOutbound.write(buffer.limit() >>> 24);
			upgradeOutbound.write(buffer.limit() >>> 16);
			upgradeOutbound.write(buffer.limit() >>> 8);
			upgradeOutbound.write(buffer.limit() & 0xFF);
		}

		// Write the content
		upgradeOutbound.write(buffer.array(), buffer.arrayOffset(),
				buffer.limit());
		upgradeOutbound.flush();

		// Reset
		if (finalFragment) {
			text = null;
			firstFrame = true;
		} else {
			firstFrame = false;
		}
		bb.clear();
	}

	/*
	 * Convert the textual message to bytes and then output it.
	 */
	private void doWriteText(CharBuffer buffer, boolean finalFragment)
			throws IOException {
		CharsetEncoder encoder = B2CConverter.UTF_8.newEncoder();
		do {
			CoderResult cr = encoder.encode(buffer, bb, true);
			if (cr.isError()) {
				cr.throwException();
			}
			bb.flip();
			if (buffer.hasRemaining()) {
				doWriteBytes(bb, false);
			} else {
				doWriteBytes(bb, finalFragment);
			}
		} while (buffer.hasRemaining());

		// Reset - bb will be cleared in doWriteBytes()
		cb.clear();
	}
}
